热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

大号|中层_同事多线程使用不当导致OOM,被我怒怼了

篇首语:本文由编程笔记#小编为大家整理,主要介绍了同事多线程使用不当导致OOM,被我怒怼了相关的知识,希望对你有一定的参考价值。目录

篇首语:本文由编程笔记#小编为大家整理,主要介绍了同事多线程使用不当导致OOM,被我怒怼了相关的知识,希望对你有一定的参考价值。


目录


  • 事故描述

  • 整体经过

  • 事故根本原因

  • 探讨问题的根源

  • 总结

事故描述

老规矩,我们先看下事故过程:某日,从 6 点 32 分开始少量用户访问 app 时会出现首页访问异常,到 7 点 20 分首页服务大规模不可用,7 点 36 分问题解决。

整体经过

事故的整个经过如下:


  • 6:58,发现报警,同时发现群里反馈首页出现网络繁忙,考虑到前几日晚上门店列表服务上线发布过,所以考虑回滚代码紧急处理问题。

  • 7:07,开始先后联系 XXX 查看解决问题。

  • 7:36,代码回滚完,服务恢复正常。

事故根本原因

事故代码模拟如下:

public static void test() throws InterruptedException, ExecutionException 
    Executor executor = Executors.newFixedThreadPool(3);
    CompletionService service &#61; new ExecutorCompletionService<>(executor);
        service.submit(new Callable() 
            &#64;Override
            public String call() throws Exception 
                return "HelloWorld--" &#43; Thread.currentThread().getName();
            
        );

先抛出问题&#xff0c;我们后面会详细阐述。问题的根源就在于 ExecutorCompletionService 结果没调用 take&#xff0c;poll 方法。

正确的写法如下所示&#xff1a;

public static void test() throws InterruptedException, ExecutionException 
    Executor executor &#61; Executors.newFixedThreadPool(3);
    CompletionService service &#61; new ExecutorCompletionService<>(executor);
    service.submit(new Callable() 
        &#64;Override
        public String call() throws Exception 
            return "HelloWorld--" &#43; Thread.currentThread().getName();
        
    );
    service.take().get();

一行代码引发的血案&#xff0c;而且不容易被发现&#xff0c;因为 OOM 是一个内存缓慢增长的过程&#xff0c;稍微粗心大意就会忽略&#xff0c;如果是这个代码块的调用量少的话&#xff0c;很可能几天甚至几个月后暴雷。

操作人回滚 or 重启服务器确实是最快的方式&#xff0c;但是如果不是事后快速分析出 OOM 的代码&#xff0c;而且不巧回滚的版本也是带 OOM 代码的&#xff0c;就比较悲催了。

如刚才所说&#xff0c;流量小了&#xff0c;回滚或者重启都可以释放内存&#xff1b;但是流量大的情况下&#xff0c;除非回滚到正常的版本&#xff0c;否则 GG。

探讨问题的根源

接下来我们来探讨问题的根源&#xff0c;为了更好的理解 ExecutorCompletionService 的 “套路”&#xff0c;我们用 ExecutorService 来作为对比&#xff0c;可以让我们更好的清楚&#xff0c;什么场景下用 ExecutorCompletionService。

先看 ExecutorService 代码&#xff1a;&#xff08;建议 down 下来跑一跑&#xff0c;以下代码建议吃饭的时候不要去看&#xff0c;味道略重&#xff01;不过便于理解 orz&#xff09;

public static void test1() throws Exception
    ExecutorService executorService &#61; Executors.newCachedThreadPool();
    ArrayList> futureArrayList &#61; new ArrayList<>();
    System.out.println("公司让你通知大家聚餐 你开车去接人");
    Future future10 &#61; executorService.submit(() -> 
        System.out.println("总裁&#xff1a;我在家上大号 我最近拉肚子比较慢 要蹲1个小时才能出来 你等会来接我吧");
        TimeUnit.SECONDS.sleep(10);
        System.out.println("总裁&#xff1a;1小时了 我上完大号了。你来接吧");
        return "总裁上完大号了";
    );
    futureArrayList.add(future10);
    Future future3 &#61; executorService.submit(() -> 
        System.out.println("研发&#xff1a;我在家上大号 我比较快 要蹲3分钟就可以出来 你等会来接我吧");
        TimeUnit.SECONDS.sleep(3);
        System.out.println("研发&#xff1a;3分钟 我上完大号了。你来接吧");
        return "研发上完大号了";
    );
    futureArrayList.add(future3);
    Future future6 &#61; executorService.submit(() -> 
        System.out.println("中层管理&#xff1a;我在家上大号  要蹲10分钟就可以出来 你等会来接我吧");
        TimeUnit.SECONDS.sleep(6);
        System.out.println("中层管理&#xff1a;10分钟 我上完大号了。你来接吧");
        return "中层管理上完大号了";
    );
    futureArrayList.add(future6);
    TimeUnit.SECONDS.sleep(1);
    System.out.println("都通知完了,等着接吧。");
    try 
        for (Future future : futureArrayList) 
            String returnStr &#61; future.get();
            System.out.println(returnStr &#43; "&#xff0c;你去接他");
        
        Thread.currentThread().join();
     catch (Exception e) 
        e.printStackTrace();
    

三个任务&#xff0c;每个任务执行时间分别是 10s、3s、6s。

通过 JDK 线程池的 submit 提交这三个 Callable 类型的任务&#xff1a;


  • step1&#xff1a;主线程把三个任务提交到线程池里面去&#xff0c;把对应返回的 Future 放到 List 里面存起来&#xff0c;然后执行“都通知完了&#xff0c;等着接吧。”这行输出语句。

  • step2&#xff1a;在循环里面执行 future.get() 操作&#xff0c;阻塞等待。


最后结果如下&#xff1a;

先通知到总裁&#xff0c;也是先接总裁&#xff0c;足足等了 1 个小时&#xff0c;接到总裁后再去接研发和中层管理&#xff0c;尽管他们早就完事儿了&#xff0c;也得等总裁拉完~~

耗时最久的-10s 异步任务最先进入 list 执行&#xff0c;所以在循环过程中获取这个 10s 的任务结果的时候&#xff0c;get 操作会一直阻塞&#xff0c;直到 10s 异步任务执行完毕。即使 3s、5s 的任务早就执行完了&#xff0c;也得阻塞等待 10s 任务执行完。

看到这里&#xff0c;尤其是做网关业务的同学可能会产生共鸣&#xff0c;一般来说网关 RPC 会调用下游 N 多个接口&#xff0c;如下图&#xff1a;

如果都按照 ExecutorService 这种方式&#xff0c;并且恰巧前几个任务调用的接口耗时比较久&#xff0c;同时阻塞等待&#xff0c;那就比较悲催了。

所以 ExecutorCompletionService 应景而出。它作为任务线程的合理管控者&#xff0c;“任务规划师”的称号名副其实。

相同场景 ExecutorCompletionService 代码&#xff1a;

public static void test2() throws Exception 
    ExecutorService executorService &#61; Executors.newCachedThreadPool();
    ExecutorCompletionService completionService &#61; new ExecutorCompletionService<>(executorService);
    System.out.println("公司让你通知大家聚餐 你开车去接人");
    completionService.submit(() -> 
        System.out.println("总裁&#xff1a;我在家上大号 我最近拉肚子比较慢 要蹲1个小时才能出来 你等会来接我吧");
        TimeUnit.SECONDS.sleep(10);
        System.out.println("总裁&#xff1a;1小时了 我上完大号了。你来接吧");
        return "总裁上完大号了";
    );
    completionService.submit(() -> 
        System.out.println("研发&#xff1a;我在家上大号 我比较快 要蹲3分钟就可以出来 你等会来接我吧");
        TimeUnit.SECONDS.sleep(3);
        System.out.println("研发&#xff1a;3分钟 我上完大号了。你来接吧");
        return "研发上完大号了";
    );
    completionService.submit(() -> 
        System.out.println("中层管理&#xff1a;我在家上大号  要蹲10分钟就可以出来 你等会来接我吧");
        TimeUnit.SECONDS.sleep(6);
        System.out.println("中层管理&#xff1a;10分钟 我上完大号了。你来接吧");
        return "中层管理上完大号了";
    );
    TimeUnit.SECONDS.sleep(1);
    System.out.println("都通知完了,等着接吧。");
    //提交了3个异步任务&#xff09;
    for (int i &#61; 0; i < 3; i&#43;&#43;) 
        String returnStr &#61; completionService.take().get();
        System.out.println(returnStr &#43; "&#xff0c;你去接他");
    
    Thread.currentThread().join();

跑完结果如下&#xff1a;

这次就相对高效了一些&#xff0c;虽然先通知的总裁&#xff0c;但是根据大家上大号的速度&#xff0c;谁先拉完先去接谁&#xff0c;不用等待上大号最久的总裁了&#xff08;现实生活里&#xff0c;建议采用第一种&#xff0c;不等总裁的后果 emmm 哈哈哈&#xff09;。

放在一起对比下输出结果&#xff1a;


两段代码的差异非常小&#xff0c;获取结果的时候 ExecutorCompletionService 使用了&#xff1a;

completionService.take().get();

为毛要用 take() 然后再 get() 呢&#xff1f;&#xff1f;&#xff1f;&#xff1f;我们看看源码&#xff1a;

| CompletionService 接口以及接口的实现类


ExecutorCompletionService 是 CompletionService 接口的实现类&#xff1a;


接着跟一下 ExecutorCompletionService 的构造方法&#xff0c;可以看到入参需要传一个线程池对象&#xff0c;默认使用的队列是 LinkedBlockingQueue&#xff0c;不过还有另外一个构造方法可以指定队列类型&#xff0c;如下两张图&#xff0c;两个构造方法。

默认 LinkedBlockingQueue 的构造方法&#xff1a;


可选队列类型的构造方法&#xff1a;


submit 任务提交的两种方式&#xff0c;都是有返回值的&#xff0c;我们例子中用到的就是第一种 Callable 类型的方法。


对比 ExecutorService 和 ExecutorCompletionService submit 方法&#xff0c;可以看出区别。

ExecutorService&#xff1a;


ExecutorCompletionService&#xff1a;


差异就在 QueueingFuture&#xff0c;这个到底作用是啥&#xff1f;


我们继续跟进去看&#xff1a;


  • QueueingFuture 继承自 FutureTask&#xff0c;而且红线部分标注的位置&#xff0c;重写了 done() 方法。

  • 把 task 放到 completionQueue 队列里面&#xff0c;当任务执行完成后&#xff0c;task 就会被放到队列里面去了。

  • 此时此刻 completionQueue 队列里面的 task 都是已经 done() 完成了的 task&#xff0c;而这个 task 就是我们拿到的一个个的 future 结果。

  • 如果调用 completionQueue 的 task 方法&#xff0c;会阻塞等待任务。等到的一定是完成了的 future&#xff0c;我们调用 .get() 方法就能立马获得结果。

看到这里&#xff0c;相信大家伙都应该多少明白点了&#xff1a;


  • 我们在使用 ExecutorService submit 提交任务后需要关注每个任务返回的 future&#xff0c;然而 CompletionService 对这些 future 进行了追踪&#xff0c;并且重写了 done 方法&#xff0c;让你等的 CompletionQueue 队列里面一定是完成了的 task。

  • 作为网关 RPC 层&#xff0c;我们不用因为某一个接口的响应慢拖累所有的请求&#xff0c;可以在处理最快响应的业务场景里使用 CompletionService。

| but&#xff0c;注意、注意、注意&#xff0c;也是本次事故的核心


当只有调用了 ExecutorCompletionService 下面的 3 个方法的任意一个时&#xff0c;阻塞队列中的 task 执行结果才会从队列中移除掉&#xff0c;释放堆内存。


由于该业务不需要使用任务的返回值&#xff0c;则没进行调用 take&#xff0c;poll 方法。从而导致没有释放堆内存&#xff0c;堆内存会随着调用量的增加一直增长。


所以&#xff0c;业务场景中不需要使用任务返回值的 别没事儿使用 CompletionService&#xff0c;假如使用了&#xff0c;记得一定要从阻塞队列中移除掉 task 执行结果&#xff0c;避免 OOM&#xff01;

总结

知道事故的原因&#xff0c;我们来总结下方法论&#xff0c;毕竟孔子他老人家说过&#xff1a;自省吾身&#xff0c;常思己过&#xff0c;善修其身&#xff01;


上线前&#xff1a;


  • 严格的代码 review 习惯&#xff0c;一定要交给 back 人去看&#xff0c;毕竟自己写的代码自己是看不出问题的&#xff0c;相信每个程序猿都有这个自信&#xff08;这个后续事故里可能会反复提到&#xff0c;很重要&#xff09;

  • 上线记录-备注好上一个可回滚的包版本&#xff08;给自己留一个后路&#xff09;

  • 上线前确认回滚后&#xff0c;业务是否可降级&#xff0c;如果不可降级&#xff0c;一定要严格拉长这次上线的监控周期


上线后&#xff1a;


  • 持续关注内存增长情况&#xff08;这部分极容易被忽略&#xff0c;大家对内存的重视度不如 CPU 使用率&#xff09;

  • 持续关注 CPU 使用率增长情况

  • GC 情况、线程数是否增长、是否有频繁的 FullGC 等

  • 关注服务性能报警&#xff0c;tp99、999 、max 是否出现明显的增高


推荐阅读
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 模板引擎StringTemplate的使用方法和特点
    本文介绍了模板引擎StringTemplate的使用方法和特点,包括强制Model和View的分离、Lazy-Evaluation、Recursive enable等。同时,还介绍了StringTemplate语法中的属性和普通字符的使用方法,并提供了向模板填充属性的示例代码。 ... [详细]
  • php缓存ri,浅析ThinkPHP缓存之快速缓存(F方法)和动态缓存(S方法)(日常整理)
    thinkPHP的F方法只能用于缓存简单数据类型,不支持有效期和缓存对象。S()缓存方法支持有效期,又称动态缓存方法。本文是小编日常整理有关thinkp ... [详细]
  • 本文整理了Java中com.evernote.android.job.JobRequest.getTransientExtras()方法的一些代码示例,展示了 ... [详细]
  • 1Lock与ReadWriteLock1.1LockpublicinterfaceLock{voidlock();voidlockInterruptibl ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • Google Play推出全新的应用内评价API,帮助开发者获取更多优质用户反馈。用户每天在Google Play上发表数百万条评论,这有助于开发者了解用户喜好和改进需求。开发者可以选择在适当的时间请求用户撰写评论,以获得全面而有用的反馈。全新应用内评价功能让用户无需返回应用详情页面即可发表评论,提升用户体验。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • MyBatis多表查询与动态SQL使用
    本文介绍了MyBatis多表查询与动态SQL的使用方法,包括一对一查询和一对多查询。同时还介绍了动态SQL的使用,包括if标签、trim标签、where标签、set标签和foreach标签的用法。文章还提供了相关的配置信息和示例代码。 ... [详细]
  • Android自定义控件绘图篇之Paint函数大汇总
    本文介绍了Android自定义控件绘图篇中的Paint函数大汇总,包括重置画笔、设置颜色、设置透明度、设置样式、设置宽度、设置抗锯齿等功能。通过学习这些函数,可以更好地掌握Paint的用法。 ... [详细]
  • 代理模式的详细介绍及应用场景
    代理模式是一种在软件开发中常用的设计模式,通过在客户端和目标对象之间增加一层中间层,让代理对象代替目标对象进行访问,从而简化系统的复杂性。代理模式可以根据不同的使用目的分为远程代理、虚拟代理、Copy-on-Write代理、保护代理、防火墙代理、智能引用代理和Cache代理等几种。本文将详细介绍代理模式的原理和应用场景。 ... [详细]
author-avatar
手机用户2502925983
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有